/**
 * @license
 * Copyright 2016 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */

/**
 * @fileoverview The AppController Class brings together the Block
 * Factory, Block Library, and Block Exporter functionality into a single web
 * app.
 *
 * @author quachtina96 (Tina Quach)
 */

/**
 * Xiaohong XeLa formatting document
 * Xiaohong XeLa Chinese document
 * Xiaohong XeLa modify many places
 */

/**
 * Controller for the Blockly Factory
 * @constructor
 */
AppController = function () {
    // Initialize Block Library
    this.blockLibraryName = 'blockLibrary';
    this.blockLibraryController =
        new BlockLibraryController(this.blockLibraryName);
    this.blockLibraryController.populateBlockLibrary();

    // Construct Workspace Factory Controller.
    this.workspaceFactoryController = new WorkspaceFactoryController
        ('workspacefactory_toolbox', 'toolbox_blocks', 'preview_blocks');

    // Initialize Block Exporter
    this.exporter =
        new BlockExporterController(this.blockLibraryController.storage);

    // Map of tab type to the div element for the tab.
    this.tabMap = Object.create(null);
    this.tabMap[AppController.BLOCK_FACTORY] =
        document.getElementById('blockFactory_tab');
    this.tabMap[AppController.WORKSPACE_FACTORY] =
        document.getElementById('workspaceFactory_tab');
    this.tabMap[AppController.EXPORTER] =
        document.getElementById('blocklibraryExporter_tab');

    // Last selected tab.
    this.lastSelectedTab = null;
    // Selected tab.
    this.selectedTab = AppController.BLOCK_FACTORY;
};

// Constant values representing the three tabs in the controller.
AppController.BLOCK_FACTORY = 'BLOCK_FACTORY';
AppController.WORKSPACE_FACTORY = 'WORKSPACE_FACTORY';
AppController.EXPORTER = 'EXPORTER';

/**
 * Tied to the 'Import Block Library' button. Imports block library from file to
 * Block Factory. Expects user to upload a single file of JSON mapping each
 * block type to its XML text representation.
 */
AppController.prototype.importBlockLibraryFromFile = function () {
    var self = this;
    var files = document.getElementById('files');
    // If the file list is empty, the user likely canceled in the dialog.
    if (files.files.length > 0) {
        BlocklyDevTools.Analytics.onImport(
            BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY,
            { format: BlocklyDevTools.Analytics.FORMAT_XML });

        // The input tag doesn't have the "multiple" attribute
        // so the user can only choose 1 file.
        var file = files.files[0];
        var fileReader = new FileReader();

        // Create a map of block type to XML text from the file when it has been
        // read.
        fileReader.addEventListener('load', function (event) {
            var fileContents = event.target.result;
            // Create empty object to hold the read block library information.
            var blockXmlTextMap = Object.create(null);
            try {
                // Parse the file to get map of block type to XML text.
                blockXmlTextMap = self.formatBlockLibraryForImport_(fileContents);
            } catch (e) {
                var message = '无法加载您的积木库文件。\n'
                window.alert(message + '\n文件名: ' + file.name);
                return;
            }

            // Create a new block library storage object with inputted block library.
            var blockLibStorage = new BlockLibraryStorage(
                self.blockLibraryName, blockXmlTextMap);

            // Update block library controller with the new block library
            // storage.
            self.blockLibraryController.setBlockLibraryStorage(blockLibStorage);
            // Update the block library dropdown.
            self.blockLibraryController.populateBlockLibrary();
            // Update the exporter's block library storage.
            self.exporter.setBlockLibraryStorage(blockLibStorage);
        });
        // Read the file.
        fileReader.readAsText(file);
    }
};

/**
 * Tied to the 'Export Block Library' button. Exports block library to file that
 * contains JSON mapping each block type to its XML text representation.
 */
AppController.prototype.exportBlockLibraryToFile = function () {
    // Get map of block type to XML.
    var blockLib = this.blockLibraryController.getBlockLibrary();
    // Concatenate the XMLs, each separated by a blank line.
    var blockLibText = this.formatBlockLibraryForExport_(blockLib);
    // Get file name.
    var filename = prompt('输入用于保存积木的文件名：' +
        'library.', 'library.xml');
    // Download file if all necessary parameters are provided.
    if (filename) {
        FactoryUtils.createAndDownloadFile(blockLibText, filename, 'xml');
        BlocklyDevTools.Analytics.onExport(
            BlocklyDevTools.Analytics.BLOCK_FACTORY_LIBRARY,
            { format: BlocklyDevTools.Analytics.FORMAT_XML });
    } else {
        var msg = '没有用于保存库的文件名，无法导出。';
        BlocklyDevTools.Analytics.onWarning(msg);
        alert(msg);
    }
};

/**
 * Converts an object mapping block type to XML to text file for output.
 * @param {!Object} blockXmlMap Object mapping block type to XML.
 * @return {string} XML text containing the block XMLs.
 * @private
 */
AppController.prototype.formatBlockLibraryForExport_ = function (blockXmlMap) {
    // Create DOM for XML.
    var xmlDom = Blockly.utils.xml.createElement('xml');

    // Append each block node to XML DOM.
    for (var blockType in blockXmlMap) {
        var blockXmlDom = Blockly.Xml.textToDom(blockXmlMap[blockType]);
        var blockNode = blockXmlDom.firstElementChild;
        xmlDom.appendChild(blockNode);
    }

    // Return the XML text.
    return Blockly.Xml.domToText(xmlDom);
};

/**
 * Converts imported block library to an object mapping block type to block XML.
 * @param {string} xmlText String representation of an XML with each block as
 *    a child node.
 * @return {!Object} Object mapping block type to XML text.
 * @private
 */
AppController.prototype.formatBlockLibraryForImport_ = function (xmlText) {
    var inputXml = Blockly.Xml.textToDom(xmlText);
    // Convert the live HTMLCollection of child Elements into a static array,
    // since the addition to editorWorkspaceXml below removes it from inputXml.
    var inputChildren = Array.from(inputXml.children);

    // Create empty map. The line below creates a  truly empty object. It doesn't
    // have built-in attributes/functions such as length or toString.
    var blockXmlTextMap = Object.create(null);

    // Populate map.
    for (var i = 0, blockNode; blockNode = inputChildren[i]; i++) {
        // Add outer XML tag to the block for proper injection in to the
        // main workspace.
        // Create DOM for XML.
        var editorWorkspaceXml = Blockly.utils.xml.createElement('xml');
        editorWorkspaceXml.appendChild(blockNode);

        xmlText = Blockly.Xml.domToText(editorWorkspaceXml);
        // All block types should be lowercase.
        var blockType = this.getBlockTypeFromXml_(xmlText).toLowerCase();
        // Some names are invalid so fix them up.
        blockType = FactoryUtils.cleanBlockType(blockType);

        blockXmlTextMap[blockType] = xmlText;
    }

    return blockXmlTextMap;
};

/**
 * Extracts out block type from XML text, the kind that is saved in block
 * library storage.
 * @param {string} xmlText A block's XML text.
 * @return {string} The block type that corresponds to the provided XML text.
 * @private
 */
AppController.prototype.getBlockTypeFromXml_ = function (xmlText) {
    var xmlDom = Blockly.Xml.textToDom(xmlText);
    // Find factory base block.
    var factoryBaseBlockXml = xmlDom.getElementsByTagName('block')[0];
    // Get field elements from factory base.
    var fields = factoryBaseBlockXml.getElementsByTagName('field');
    for (var i = 0; i < fields.length; i++) {
        // The field whose name is 'NAME' holds the block type as its value.
        if (fields[i].getAttribute('name') == 'NAME') {
            return fields[i].childNodes[0].nodeValue;
        }
    }
};

/**
 * Add click handlers to each tab to allow switching between the Block Factory,
 * Workspace Factory, and Block Exporter tab.
 * @param {!Object} tabMap Map of tab name to div element that is the tab.
 */
AppController.prototype.addTabHandlers = function (tabMap) {
    var self = this;
    for (var tabName in tabMap) {
        var tab = tabMap[tabName];
        // Use an additional closure to correctly assign the tab callback.
        tab.addEventListener('click', self.makeTabClickHandler_(tabName));
    }
};

/**
 * Set the selected tab.
 * @param {string} tabName AppController.BLOCK_FACTORY,
 *    AppController.WORKSPACE_FACTORY, or AppController.EXPORTER
 * @private
 */
AppController.prototype.setSelected_ = function (tabName) {
    this.lastSelectedTab = this.selectedTab;
    this.selectedTab = tabName;
};

/**
 * Creates the tab click handler specific to the tab specified.
 * @param {string} tabName AppController.BLOCK_FACTORY,
 *    AppController.WORKSPACE_FACTORY, or AppController.EXPORTER
 * @return {!Function} The tab click handler.
 * @private
 */
AppController.prototype.makeTabClickHandler_ = function (tabName) {
    var self = this;
    return function () {
        self.setSelected_(tabName);
        self.onTab();
    };
};

/**
 * Called on each tab click. Hides and shows specific content based on which tab
 * (Block Factory, Workspace Factory, or Exporter) is selected.
 */
AppController.prototype.onTab = function () {
    // Get tab div elements.
    var blockFactoryTab = this.tabMap[AppController.BLOCK_FACTORY];
    var exporterTab = this.tabMap[AppController.EXPORTER];
    var workspaceFactoryTab = this.tabMap[AppController.WORKSPACE_FACTORY];

    // Warn user if they have unsaved changes when leaving Block Factory.
    if (this.lastSelectedTab == AppController.BLOCK_FACTORY &&
        this.selectedTab != AppController.BLOCK_FACTORY) {
        var hasUnsavedChanges =
            !FactoryUtils.savedBlockChanges(this.blockLibraryController);
        if (hasUnsavedChanges) {
            BlocklyDevTools.Analytics.onWarning(msg);
            // If the user doesn't want to switch tabs with unsaved changes,
            // stay on Block Factory Tab.
            this.setSelected_(AppController.BLOCK_FACTORY);
            this.lastSelectedTab = AppController.BLOCK_FACTORY;
            return;
        }
    }

    // Only enable key events in workspace factory if workspace factory tab is
    // selected.
    this.workspaceFactoryController.keyEventsEnabled =
        this.selectedTab == AppController.WORKSPACE_FACTORY;

    // Turn selected tab on and other tabs off.
    this.styleTabs_();

    if (this.selectedTab == AppController.EXPORTER) {
        BlocklyDevTools.Analytics.onNavigateTo('Exporter');

        // Hide other tabs.
        FactoryUtils.hide('workspaceFactoryContent');
        FactoryUtils.hide('blockFactoryContent');
        // Show exporter tab.
        FactoryUtils.show('blockLibraryExporter');

        // Need accurate state in order to know which blocks are used in workspace
        // factory.
        this.workspaceFactoryController.saveStateFromWorkspace();

        // Update exporter's list of the types of blocks used in workspace factory.
        var usedBlockTypes = this.workspaceFactoryController.getAllUsedBlockTypes();
        this.exporter.setUsedBlockTypes(usedBlockTypes);

        // Update exporter's block selector to reflect current block library.
        this.exporter.updateSelector();

        // Update the exporter's preview to reflect any changes made to the blocks.
        this.exporter.updatePreview();

    } else if (this.selectedTab == AppController.BLOCK_FACTORY) {
        BlocklyDevTools.Analytics.onNavigateTo('BlockFactory');

        // Hide other tabs.
        FactoryUtils.hide('blockLibraryExporter');
        FactoryUtils.hide('workspaceFactoryContent');
        // Show Block Factory.
        FactoryUtils.show('blockFactoryContent');

    } else if (this.selectedTab == AppController.WORKSPACE_FACTORY) {
        // TODO: differentiate Workspace and Toolbox editor, based on the other tab state.
        BlocklyDevTools.Analytics.onNavigateTo('WorkspaceFactory');

        // Hide other tabs.
        FactoryUtils.hide('blockLibraryExporter');
        FactoryUtils.hide('blockFactoryContent');
        // Show workspace factory container.
        FactoryUtils.show('workspaceFactoryContent');
        // Update block library category.
        var categoryXml = this.exporter.getBlockLibraryCategory();
        var blockTypes = this.blockLibraryController.getStoredBlockTypes();
        this.workspaceFactoryController.setBlockLibCategory(categoryXml,
            blockTypes);
    }

    // Resize to render workspaces' toolboxes correctly for all tabs.
    window.dispatchEvent(new Event('resize'));
};

/**
 * Called on each tab click. Styles the tabs to reflect which tab is selected.
 * @private
 */
AppController.prototype.styleTabs_ = function () {
    for (var tabName in this.tabMap) {
        if (this.selectedTab == tabName) {
            this.tabMap[tabName].classList.replace('taboff', 'tabon');
        } else {
            this.tabMap[tabName].classList.replace('tabon', 'taboff');
        }
    }
};

/**
 * Assign button click handlers for the exporter.
 */
AppController.prototype.assignExporterClickHandlers = function () {
    var self = this;
    document.getElementById('button_setBlocks').addEventListener('click',
        function () {
            self.openModal('dropdownDiv_setBlocks');
        });

    document.getElementById('dropdown_addAllUsed').addEventListener('click',
        function () {
            self.exporter.selectUsedBlocks();
            self.exporter.updatePreview();
            self.closeModal();
        });

    document.getElementById('dropdown_addAllFromLib').addEventListener('click',
        function () {
            self.exporter.selectAllBlocks();
            self.exporter.updatePreview();
            self.closeModal();
        });

    document.getElementById('clearSelectedButton').addEventListener('click',
        function () {
            self.exporter.clearSelectedBlocks();
            self.exporter.updatePreview();
        });

    // Export blocks when the user submits the export settings.
    document.getElementById('exporterSubmitButton').addEventListener('click',
        function () {
            self.exporter.export();
        });
};

AppController.prototype.updateExporterSubmitButton = function () {
    var exporterSubmitButton = document.getElementById('exporterSubmitButton');
    var blockDefCheck = document.getElementById('blockDefCheck');
    var genStubCheck = document.getElementById('genStubCheck');
    exporterSubmitButton.innerText = `导出（${blockDefCheck.checked + genStubCheck.checked}个文件）`
    exporterSubmitButton.disabled = blockDefCheck.checked + genStubCheck.checked === 0
}

/**
 * Assign change listeners for the exporter. These allow for the dynamic update
 * of the exporter preview.
 */
AppController.prototype.assignExporterChangeListeners = function () {
    var self = this;

    var blockDefCheck = document.getElementById('blockDefCheck');
    var genStubCheck = document.getElementById('genStubCheck');

    // Select the block definitions and generator stubs on default.
    blockDefCheck.checked = true;
    genStubCheck.checked = true;

    // Checking the block definitions checkbox displays preview of code to export.
    document.getElementById('blockDefCheck').addEventListener('change',
        function (e) {
            self.updateExporterSubmitButton()
            self.ifCheckedEnable(blockDefCheck.checked,
                ['blockDefs', 'blockDefSettings']);
        });

    // Preview updates when user selects different block definition format.
    document.getElementById('exportFormat').addEventListener('change',
        function (e) {
            self.updateExporterSubmitButton()
            self.exporter.updatePreview();
        });

    // Checking the generator stub checkbox displays preview of code to export.
    document.getElementById('genStubCheck').addEventListener('change',
        function (e) {
            self.updateExporterSubmitButton()
            self.ifCheckedEnable(genStubCheck.checked,
                ['genStubs', 'genStubSettings']);
        });

    // Preview updates when user selects different generator stub language.
    document.getElementById('exportLanguage').addEventListener('change',
        function (e) {
            self.exporter.updatePreview();
        });
};

/**
 * If given checkbox is checked, enable the given elements.  Otherwise, disable.
 * @param {boolean} enabled True if enabled, false otherwise.
 * @param {!Array.<string>} idArray Array of element IDs to enable when
 *    checkbox is checked.
 */
AppController.prototype.ifCheckedEnable = function (enabled, idArray) {
    for (var i = 0, id; id = idArray[i]; i++) {
        var element = document.getElementById(id);
        if (enabled) {
            element.classList.remove('disabled');
        } else {
            element.classList.add('disabled');
        }
        var fields = element.querySelectorAll('input, textarea, select');
        for (var j = 0, field; field = fields[j]; j++) {
            field.disabled = !enabled;
        }
    }
};

/**
 * Assign button click handlers for the block library.
 */
AppController.prototype.assignLibraryClickHandlers = function () {
    var self = this;

    // Button for saving block to library.
    document.getElementById('saveToBlockLibraryButton').addEventListener('click',
        function () {
            self.blockLibraryController.saveToBlockLibrary();
        });

    // Button for removing selected block from library.
    document.getElementById('removeBlockFromLibraryButton').addEventListener(
        'click',
        function () {
            self.blockLibraryController.removeFromBlockLibrary();
        });

    // Button for clearing the block library.
    document.getElementById('clearBlockLibraryButton').addEventListener('click',
        function () {
            self.blockLibraryController.clearBlockLibrary();
        });

    // Hide and show the block library dropdown.
    document.getElementById('button_blockLib').addEventListener('click',
        function () {
            self.openModal('dropdownDiv_blockLib');
        });
};

/**
 * Assign button click handlers for the block factory.
 */
AppController.prototype.assignBlockFactoryClickHandlers = function () {
    var self = this;
    // Assign button event handlers for Block Factory.
    document.getElementById('localSaveButton')
        .addEventListener('click', function () {
            self.exportBlockLibraryToFile();
        });

    document.getElementById('files').addEventListener('change',
        function () {
            // Warn user.
            var replace = confirm('该导入的积木库将替换当前的积木库。');
            if (replace) {
                self.importBlockLibraryFromFile();
                // Clear this so that the change event still fires even if the
                // same file is chosen again. If the user re-imports a file, we
                // want to reload the workspace with its contents.
                this.value = null;
            }
        });

    document.getElementById('createNewBlockButton')
        .addEventListener('click', function () {
            // If there are unsaved changes warn user, check if they'd like to
            // proceed with unsaved changes, and act accordingly.
            var proceedWithUnsavedChanges =
                self.blockLibraryController.warnIfUnsavedChanges();
            if (!proceedWithUnsavedChanges) {
                return;
            }

            BlockFactory.showStarterBlock();
            self.blockLibraryController.setNoneSelected();

            // Close the Block Library Dropdown.
            self.closeModal();
        });
};

/**
 * Add event listeners for the block factory.
 */
AppController.prototype.addBlockFactoryEventListeners = function () {
    // Update code on changes to block being edited.
    BlockFactory.mainWorkspace.addChangeListener(BlockFactory.updateLanguage);

    // Disable blocks not attached to the factory_base block.
    BlockFactory.mainWorkspace.addChangeListener(Blockly.Events.disableOrphans);

    // Update the buttons on the screen based on whether
    // changes have been saved.
    var self = this;
    BlockFactory.mainWorkspace.addChangeListener(function () {
        self.blockLibraryController.updateButtons(FactoryUtils.savedBlockChanges(
            self.blockLibraryController));
    });

    document.getElementById('direction')
        .addEventListener('change', BlockFactory.updatePreview);
    document.getElementById('languageTA')
        .addEventListener('change', BlockFactory.manualEdit);
    document.getElementById('languageTA')
        .addEventListener('keyup', BlockFactory.manualEdit);
    document.getElementById('format')
        .addEventListener('change', BlockFactory.formatChange);
    document.getElementById('language')
        .addEventListener('change', BlockFactory.updatePreview);
};

/**
 * Handle Blockly Storage with App Engine.
 */
AppController.prototype.initializeBlocklyStorage = function () {
    BlocklyStorage.HTTPREQUEST_ERROR =
        '请求有问题。\n';
    BlocklyStorage.LINK_ALERT =
        '通过此公共链接共享您的积木。如果一年不用，我们会删除它们。它们与您的帐户无关，按照Google的隐私政策处理。请确保不要包含任何私人信息：\n\n%1';
    BlocklyStorage.HASH_ERROR =
        '非常抱歉，"%1" 不符合任何保存的积木文件。';
    BlocklyStorage.XML_ERROR = '无法加载您保存的文件。\n' +
        '也许它是用不同版本的 Blockly 创建的？';
    // var linkButton = document.getElementById('linkButton');
    // linkButton.style.display = 'inline-block';
    // linkButton.addEventListener('click',
    //     function () {
    //         BlocklyStorage.link(BlockFactory.mainWorkspace);
    //     });
    BlockFactory.disableEnableLink();
};

/**
 * Handle resizing of elements.
 */
AppController.prototype.onresize = function (event) {
    if (this.selectedTab == AppController.BLOCK_FACTORY) {
        // Handle resizing of Block Factory elements.
        var expandList = [
            document.getElementById('blocklyPreviewContainer'),
            document.getElementById('blockly'),
            document.getElementById('blocklyMask'),
            document.getElementById('preview'),
            document.getElementById('languagePre'),
            document.getElementById('languageTA'),
            document.getElementById('generatorPre'),
        ];
        for (var i = 0, expand; expand = expandList[i]; i++) {
            expand.style.width = (expand.parentNode.offsetWidth - 2) + 'px';
            expand.style.height = (expand.parentNode.offsetHeight - 2) + 'px';
        }
    } else if (this.selectedTab == AppController.EXPORTER) {
        // Handle resize of Exporter block options.
        this.exporter.view.centerPreviewBlocks();
    }
};

/**
 * Handler for the window's 'beforeunload' event. When a user has unsaved
 * changes and refreshes or leaves the page, confirm that they want to do so
 * before actually refreshing.
 * @param {!Event} e beforeunload event.
 */
AppController.prototype.confirmLeavePage = function (e) {
    BlocklyDevTools.Analytics.sendQueued();
    if ((!BlockFactory.isStarterBlock() &&
        !FactoryUtils.savedBlockChanges(blocklyFactory.blockLibraryController)) ||
        blocklyFactory.workspaceFactoryController.hasUnsavedChanges()) {
        var confirmationMessage = '您将丢失所有未保存的更改。\n' +
            '您确定要退出此页面吗？';
        BlocklyDevTools.Analytics.onWarning(confirmationMessage);
        e.returnValue = confirmationMessage;
        return confirmationMessage;
    }
};

/**
 * Show a modal element, usually a dropdown list.
 * @param {string} id ID of element to show.
 */
AppController.prototype.openModal = function (id) {
    Blockly.hideChaff();
    this.modalName_ = id;
    document.getElementById(id).style.display = 'block';
    document.getElementById('modalShadow').style.display = 'block';
};

/**
 * Hide a previously shown modal element.
 */
AppController.prototype.closeModal = function () {
    var id = this.modalName_;
    if (!id) {
        return;
    }
    document.getElementById(id).style.display = 'none';
    document.getElementById('modalShadow').style.display = 'none';
    this.modalName_ = null;
};

/**
 * Name of currently open modal.
 * @type {string?}
 * @private
 */
AppController.prototype.modalName_ = null;

/**
 * Initialize Blockly and layout.  Called on page load.
 */
AppController.prototype.init = function () {
    var self = this;
    // Handle Blockly Storage with App Engine.
    if ('BlocklyStorage' in window) {
        this.initializeBlocklyStorage();
    }

    // Assign click handlers.
    this.assignExporterClickHandlers();
    this.assignLibraryClickHandlers();
    this.assignBlockFactoryClickHandlers();
    // Hide and show the block library dropdown.
    document.getElementById('modalShadow').addEventListener('click',
        function () {
            self.closeModal();
        });

    this.onresize();
    window.addEventListener('resize', function () {
        self.onresize();
    });

    // Inject Block Factory Main Workspace.
    var toolbox = document.getElementById('blockfactory_toolbox');
    BlockFactory.mainWorkspace = Blockly.inject('blockly',
        {
            collapse: false,
            toolbox: toolbox,
            comments: false,
            disable: false,
            media: '../../media/',
            renderer: "zelos",
            zoom: {
                // 缩放设置
                controls: true, // 显示缩放按钮控件
                wheel: true, // 允许使用鼠标滚轮缩放
                startScale: 1, // 初始积木大小
                maxScale: 3, // 最大大小
                minScale: 0.3, // 最小大小
            },
            move: {
                // 移动设置
                wheel: true, // 允许鼠标滚轮滑动
            },
        });

    // Add tab handlers for switching between Block Factory and Block Exporter.
    this.addTabHandlers(this.tabMap);

    // Assign exporter change listeners.
    this.assignExporterChangeListeners();

    // Create the root block on Block Factory main workspace.
    if ('BlocklyStorage' in window && window.location.hash.length > 1) {
        BlocklyStorage.retrieveXml(window.location.hash.substring(1),
            BlockFactory.mainWorkspace);
    } else {
        BlockFactory.showStarterBlock();
    }
    BlockFactory.mainWorkspace.clearUndo();

    // Add Block Factory event listeners.
    this.addBlockFactoryEventListeners();

    // Workspace Factory init.
    WorkspaceFactoryInit.initWorkspaceFactory(this.workspaceFactoryController);

    BlockFactory.mainWorkspace.addChangeListener(function (e) {
        // 积木盒开/关特效
        if (e.type == "toolbox_item_select") {
            const s = $($("table#blockFactoryContent>tbody>tr>td#blocklyWorkspaceContainer>#blockly>.injectionDiv>.blocklyFlyout")[1]); // Flyout元素
            if (e.newItem != null && e.oldItem == null) {
                // 如果是打开
                console.log("open");
                s.css("display", "block");
                s.css("transform", "translate(72px, 0px)");
            } else if (e.newItem == null && e.oldItem != null) {
                // 如果是关闭
                s.css("display", "block");
                s.css("transform", `translate(-${s.width() + 1}px, 0px)`);
                console.log("close");
            }
        }

        // 当有积木开始拖动
        if (e.type == "drag" && !!e.isStart) {
            // 显示“删除垃圾桶”
            $(".delect-block-zone").addClass("delect-block-zone-show");
        }

        // 当有积木结束拖动
        if (e.type == "drag" && !e.isStart) {
            // 隐藏“删除垃圾桶”
            $(".delect-block-zone")[0].classList.remove("delect-block-zone-show");
        }

        !function () {
            let a = Object.values(document.querySelectorAll("*")).filter(v => v.id.startsWith("blocklySelectedGlowFilter"));
            a.forEach(v => {
                v.querySelector("feFlood").setAttribute("flood-color", "#fff");
            })
            let b = Object.values(document.querySelectorAll("*")).filter(v => v.id.startsWith("blocklyReplacementGlowFilter"));
            b.forEach(v => {
                v.querySelector("feFlood").setAttribute("flood-color", "#fff");
            })
        }();
    });

    const color = {
        operation: "#feae8a",
        loop: "#68cdff",
        control: "#68cdff",
        math: "#feae8a",
        procedure: "#f88767",
        function: "#77d657",
        varible: "#ffbb55",
        list: "#f9cc37",
        colour: "#2bc9a7",
    };
    window.BlocklyTheme = Blockly.Theme.defineTheme("BlocklyTheme", {
        base: Blockly.Themes.Classic,
        blockStyles: {
            loop_blocks: { colourPrimary: color["loop"] },
            math_blocks: { colourPrimary: color["math"] },
            logic_blocks: { colourPrimary: color["math"] },
            text_blocks: { colourPrimary: color["math"] },
            procedure_blocks: { colourPrimary: color["procedure"] },
            function_blocks: { colourPrimary: color["function"] },
            variable_blocks: { colourPrimary: color["varible"] },
            list_blocks: { colourPrimary: color["list"] },
            colour_blocks: { colourPrimary: color["colour"] },
        },
        componentStyles: {
            workspaceBackgroundColour: "#fafafa",
            toolboxBackgroundColour: "#fff",
            flyoutBackgroundColour: "#fff",
            toolboxForegroundColour: "#000",
            scrollbarColour: "#ccc",
        },
    });

    BlockFactory.mainWorkspace.setTheme(window.BlocklyTheme);
};
